package com.atguigu.tingshu.search.service.impl;

import cn.hutool.core.util.IdUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldSort;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Operator;
import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Highlight;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import com.alibaba.fastjson.JSON;
import com.atguigu.tingshu.album.client.AlbumInfoFeignClient;
import com.atguigu.tingshu.album.client.CategoryFeignClient;
import com.atguigu.tingshu.common.cache.GuiguCache;
import com.atguigu.tingshu.common.config.pool.ThreadPoolConfig;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AssertUtil;
import com.atguigu.tingshu.common.util.PinYinUtils;
import com.atguigu.tingshu.model.album.AlbumInfo;
import com.atguigu.tingshu.model.album.BaseCategory3;
import com.atguigu.tingshu.model.album.BaseCategoryView;
import com.atguigu.tingshu.model.search.AlbumInfoIndex;
import com.atguigu.tingshu.model.search.AttributeValueIndex;
import com.atguigu.tingshu.model.search.SuggestIndex;
import com.atguigu.tingshu.query.search.AlbumIndexQuery;
import com.atguigu.tingshu.search.service.SearchService;
import com.atguigu.tingshu.user.client.UserInfoFeignClient;
import com.atguigu.tingshu.vo.album.AlbumListVo;
import com.atguigu.tingshu.vo.album.AlbumStatVo;
import com.atguigu.tingshu.vo.search.AlbumInfoIndexVo;
import com.atguigu.tingshu.vo.search.AlbumSearchResponseVo;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import jakarta.servlet.http.HttpServletRequest;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
import org.springframework.data.elasticsearch.client.elc.QueryBuilders;
import org.springframework.data.elasticsearch.core.suggest.Completion;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

@Slf4j
@Service
@SuppressWarnings({"unchecked"})
public class SearchServiceImpl implements SearchService {
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
    //idea不识别spring组件注解以外的组件类(Mapper、Feign客户端)
    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;
    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    @Autowired
    private CategoryFeignClient categoryFeignClient;
    @Autowired
    private ElasticsearchClient elasticsearchClient;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RedisTemplate redisTemplate;

    //首页数据查询的业务
    @Override
    @SneakyThrows
    public List<Map> getChannel(Long c1Id) {
        //1、根据c1Id查询它下面的三级分类列表
        Result<List<BaseCategory3>> c3sResult = categoryFeignClient.findCategory3sByC1Id(c1Id);
        if (c3sResult == null
                || !c3sResult.getCode().equals(ResultCodeEnum.SUCCESS.getCode())
        ) {
            throw new GuiguException(ResultCodeEnum.DATA_ERROR);
        }
        List<BaseCategory3> category3s = c3sResult.getData();
        //将 三级分类集合 转为 id映射分类对象的map
        Map<Long, BaseCategory3> category3Map = category3s.stream()
                .collect(Collectors.toMap(BaseCategory3::getId, baseCategory3 -> baseCategory3));
        List<FieldValue> fieldValues = category3s.stream().map(c3 -> FieldValue.of(c3.getId())
        ).collect(Collectors.toList());
        //2、查询三级分类下的热门专辑集合
        SearchRequest.Builder builder = new SearchRequest.Builder();
        builder.index("albuminfo");
        //设置查询条件
        builder.query(q-> q.terms(ts->ts.field("category3Id").terms(t->t.value(fieldValues))));
        builder.size(0);//不直接查询数据，在聚合中通过tophit查询

        //聚合查询
        builder.aggregations("c3IdAgg",
                agg1-> agg1.terms(a1t-> a1t.field("category3Id"))
                    .aggregations("topN",
                            agg2-> agg2.topHits(th-> th.size(6)
                                    .sort(agg2S-> agg2S
                                            .field(FieldSort.of(fs->fs.field("hotScore").order(SortOrder.Desc))))

                    ) )
        );

        SearchRequest build = builder.build();
//        System.out.println(build);
        log.info("查询首页数据的DSL：{}",build);
        SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(build, AlbumInfoIndex.class);
        //获取分类id聚合的桶： 每个桶 对应一个Map
        Aggregate c3IdAgg = response.aggregations().get("c3IdAgg");
        List<Map> list = new ArrayList<>();//遍历桶集合
        // es 使用基本类型聚合时 封装的词条结果是基本类型的
        // long类型 使用LTerms   keyword类型 STerms
        c3IdAgg.lterms().buckets().array().forEach(c3IdBucket-> {
//            System.out.println(c3IdBucket);
            Map  map = new HashMap<>();

            List<AlbumInfoIndex> albumInfoIndices = new ArrayList<>();
            //获取分类桶中 聚合的 topN结果
            c3IdBucket.aggregations()
                    .get("topN").topHits()
                    .hits().hits().forEach(h->{
                        AlbumInfoIndex infoIndex = h.source().to(AlbumInfoIndex.class);
                        albumInfoIndices.add(infoIndex);
                    });
            /*
                每个map存
                    一个三级分类id映射的三级分类对象
                        list映射自己的专辑列表
             */
            map.put("baseCategory3", category3Map.get(c3IdBucket.key()));//三级分类列表
            map.put("list", albumInfoIndices);//三级分类列表
            list.add(map);
        });

        return list;
    }

    @Override
    @SneakyThrows
    public List<String> completeSuggest(String keyword) {
        log.error("我是周少，我为自己打印错误,keyword:{}",keyword);
        //1、初始化 搜索推荐 索引结构
        // 使用SuggestIndex 实体类 初始化索引库
        //   - 可以利用elasticsearchTemplate 初始化： 一般在单元测试执行
        //   - 也可以利用 springdata Repository模板类 创建一个索引库操作的Repository，项目启动时会自动根据实体类初始化索引库

        //2、数据上架时： 可以将文档分词后存到 搜索推荐的索引库中
        //3、前端 首页 输入搜索关键字时 可以提交请求 查询搜索推荐的字符串列表展示
        SearchRequest.Builder builder = new SearchRequest.Builder();
        builder.suggest(
                s->
                    // 执行获取的搜索推荐 列表的名称为：sKeyword
                    s.suggesters("sKeyword",
                            //指定使用前端提交的keyword变量的值 进行匹配
                            s1-> s1.prefix(keyword)
                                    //补齐 搜索推荐的配置
                                    .completion(
                                            s1c-> s1c
                                                    //使用索引库中的keyword字段匹配
                                                    .field("keyword")
                                                    //去重
                                                    .skipDuplicates(true)
                                                    //查询前10条
                                                    .size(10)
                                                    //模糊匹配： 编辑次数为1
                                                    .fuzzy(s1cf->s1cf.fuzziness("auto"))
                                    )
                        )
                    .suggesters("sKeywordPinyin" ,
                            s2->s2.prefix(keyword)
                                    .completion(
                                            s1c-> s1c
                                                    .field("keywordPinyin")
                                                    .skipDuplicates(true)
                                                    .size(10)
                                                    .fuzzy(s1cf->s1cf.fuzziness("auto"))
                                    )
                            )
                        .suggesters("sKeywordSequence" ,
                                s3->s3.prefix(keyword)
                                        .completion(
                                                s1c-> s1c
                                                        .field("keywordSequence")
                                                        .skipDuplicates(true)
                                                        .size(10)
                                                        .fuzzy(s1cf->s1cf.fuzziness("auto"))
                                        )
                        )
        );

        SearchResponse<SuggestIndex> response = elasticsearchClient.search(builder.build(), SuggestIndex.class);
        //用来接收搜索推荐的词条
        List<String> list = new ArrayList<>();
        List<Suggestion<SuggestIndex>> sKeyword = response.suggest().get("sKeyword");
        sKeyword.forEach(sk->{
            sk.completion().options().forEach(suggest->{
                list.add(suggest.text());
            });
        });
        List<Suggestion<SuggestIndex>> sKeywordPinyin = response.suggest().get("sKeywordPinyin");
        sKeywordPinyin.forEach(sk->{
            sk.completion().options().forEach(suggest->{
                list.add(suggest.text());
            });
        });
        List<Suggestion<SuggestIndex>> sKeywordSequence = response.suggest().get("sKeywordSequence");
        sKeywordSequence.forEach(sk->{
            sk.completion().options().forEach(suggest->{
                list.add(suggest.text());
            });
        });
        return list;
    }
    /*
        专辑详情页数据：
            1、专辑详情
            2、主播信息
            3、分类信息
            4、统计

        声音分页列表： 第二个异步请求加载
     */
    @GuiguCache(lockKeyPrefix = RedisConstant.ALBUM_CACHE_LOCK_KEY,
            cacheKeyPrefix = RedisConstant.ALBUM_CACHE_KEY,
            cacheExpireTime = RedisConstant.CACHE_TIMEOUT ,
            bloomFilterKey = RedisConstant.ALBUM_BLOOM_FILTER,
            isNeedBloomFilter = true
    )
    @Override
    public Map getAlbumInfoById(Long albumId) {
        Map map = new HashMap();
        //1、专辑详情  AlbumInfo
        // TODO 根据专辑id远程调用service-album 查询
        /*
            内部调用接口 需要校验登录状态的解决？？
                远程访问的接口需要校验请求头中的 token判断是否登录

         */
        //主线程中获取请求报文中的token
//        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
//                .getRequestAttributes();
//        HttpServletRequest request = attributes.getRequest();
        CompletableFuture<AlbumInfo> c1 = CompletableFuture.supplyAsync(() -> {
            Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.findAlbumInfo(albumId);
            //断言：判断返回结果是否成功
            AssertUtil.resultAssert(albumInfoResult, ResultCodeEnum.DATA_ERROR);

            AlbumInfo albumInfo = albumInfoResult.getData();
            map.put("albumInfo",albumInfo);

            return albumInfo;
        }, ThreadPoolConfig.getExecutor());

        //2、专辑统计数据 AlbumStatVo
        CompletableFuture<Void> c2 = CompletableFuture.runAsync(() -> {
            // 根据专辑id远程调用service-album 查询
            Result<AlbumStatVo> albumStatVoResult = albumInfoFeignClient.getAlbumStatVo(albumId);
            AssertUtil.resultAssert(albumStatVoResult, ResultCodeEnum.DATA_ERROR);

            AlbumStatVo albumStatVo = albumStatVoResult.getData();
            map.put("albumStatVo",albumStatVo);

        }, ThreadPoolConfig.getExecutor());


        //3、专辑主播信息 Annourcer
        CompletableFuture<Void> c3 = c1.thenAcceptAsync(albumInfo -> {
            // 根据专辑详情中的主播id 远程调用service-user 查询
            Result<UserInfoVo> userInfoVoResult = userInfoFeignClient
                    .getUserInfo(albumInfo.getUserId());
            AssertUtil.resultAssert(userInfoVoResult, ResultCodeEnum.DATA_ERROR);

            UserInfoVo userInfoVo = userInfoVoResult.getData();
            map.put("announcer",userInfoVo);
        }, ThreadPoolConfig.getExecutor());

        //4、专辑所属的123级分类数据  BaseCategoryView
        CompletableFuture<Void> c4 = c1.thenAcceptAsync(albumInfo -> {
            // 根据专辑详情中的三级分类id 远程调用service-album 查询
            Result<BaseCategoryView> categoryViewResult = categoryFeignClient.findAttributeByC3Id(albumInfo.getCategory3Id());
            BaseCategoryView categoryView = categoryViewResult.getData();
            map.put("baseCategoryView",categoryView);
        }, ThreadPoolConfig.getExecutor());

        CompletableFuture.allOf(c1,c2,c3,c4).join();
        return map;
    }
    @SneakyThrows
    @Override
    public List<AlbumInfoIndexVo> findRankingList(Long c1Id, String sort) {
        List<AlbumInfoIndexVo> albumInfoIndexVos = (List<AlbumInfoIndexVo>) redisTemplate.boundHashOps(RedisConstant.RANKING_KEY_PREFIX + c1Id)
                .get(sort);
        return albumInfoIndexVos;
//        SearchRequest.Builder builder = new SearchRequest.Builder();
//        builder.query(q-> q.term(tq->tq.field("category1Id")
//                .value(c1Id)));
//        builder.sort(s->s.field(f->f.field(sort).order(SortOrder.Desc)));
//        builder.size(10);
//        SearchResponse<AlbumInfoIndex> response = elasticsearchClient
//                .search(builder.build(), AlbumInfoIndex.class);
//        List<AlbumInfoIndexVo> albumInfoIndexVos = new ArrayList<>();
//        response.hits().hits()
//                .forEach(albumInfoIndexHit -> {
//                    AlbumInfoIndex infoIndex = albumInfoIndexHit.source();
//                    AlbumInfoIndexVo vo = new AlbumInfoIndexVo();
//                    BeanUtils.copyProperties(infoIndex , vo);
//                    albumInfoIndexVos.add(vo);
//                });


    }
    @SneakyThrows
    @Override
    public void updateLatelyAlbumRanking() {
        //1、加载所有的1级分类id
        Result<List<Long>> result = categoryFeignClient.findAllC1Ids();
        AssertUtil.resultAssert(result,ResultCodeEnum.DATA_ERROR);
        List<Long> c1Ids = result.getData();
        //2、根据1级分类id 和它下面的每一个热门排序方式 查询数据 缓存
        c1Ids.forEach(c1Id->{
            try {
                String[] strs = new String[]{"playStatNum","subscribeStatNum","buyStatNum",
                        "commentStatNum","hotScore"};
                for (String str : strs) {
                    SearchRequest.Builder builder = new SearchRequest.Builder();
                    builder.query(q-> q.term(tq->tq.field("category1Id")
                            .value(c1Id)));
                    builder.sort(s->s.field(f->f.field(str).order(SortOrder.Desc)));
                    builder.size(10);
                    SearchResponse<AlbumInfoIndex> response = elasticsearchClient
                            .search(builder.build(), AlbumInfoIndex.class);
                    List<AlbumInfoIndexVo> albumInfoIndexVos = new ArrayList<>();
                    response.hits().hits()
                            .forEach(albumInfoIndexHit -> {
                                AlbumInfoIndex infoIndex = albumInfoIndexHit.source();
                                AlbumInfoIndexVo vo = new AlbumInfoIndexVo();
                                BeanUtils.copyProperties(infoIndex , vo);
                                albumInfoIndexVos.add(vo);
                            });

                    //c1Id -> hotScore -> List<AlbumInfoIndexVo> albumInfoIndexVos
                    //redis可以使用hash结构存储： c1Id拼接hash的key ， hotScore当作hash的键， albumInfoIndexVos当作hash的值
                    //热门数据： 以后学了分布式定时任务，会通过定时任务闲时更新
                    redisTemplate
                            .boundHashOps(RedisConstant.RANKING_KEY_PREFIX+c1Id)
                            .put(str,albumInfoIndexVos);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        });


    }
    //专辑上架
    @Override
    public void upperAlbum(Long albumId) {
        if (albumId == null) {
            throw new GuiguException(ResultCodeEnum.DATA_ERROR);
        }
        AlbumInfoIndex albumInfoIndex = new AlbumInfoIndex();
        //1和3并行    2、4等1结束后执行(1   2+4 串行)
        CompletableFuture<AlbumListVo> c1 = CompletableFuture.supplyAsync(() -> {
            //执行数据导入es的业务代码
            //1、根据albumId查询AlbumListVo对象
            Result<AlbumListVo> listVoResult = albumInfoFeignClient.getAlbumListVo(albumId);
//            if (listVoResult == null
//                    || !listVoResult.getCode().equals(ResultCodeEnum.SUCCESS.getCode())
//                    || listVoResult.getData() == null) {
//                log.error("上架专辑导入数据，获取专辑数据失败,专辑id:{}", albumId);
//                throw new GuiguException(ResultCodeEnum.DATA_ERROR);
//            }
            AssertUtil.resultAssert(listVoResult , ResultCodeEnum.DATA_ERROR);
            AlbumListVo albumListVo = listVoResult.getData();

            BeanUtils.copyProperties(albumListVo, albumInfoIndex);
            albumInfoIndex.setId(albumListVo.getAlbumId());
            //4、专辑统计数据/热度：默认都是0 可以生成随机的统计数据使用
            BigDecimal hotScore =
                    new BigDecimal("0.4")
                            .multiply(new BigDecimal(albumInfoIndex.getBuyStatNum()))
                            .add(
                                    new BigDecimal("0.3")
                                            .multiply(new BigDecimal(albumInfoIndex.getSubscribeStatNum()))
                            )
                            .add(
                                    new BigDecimal("0.2")
                                            .multiply(new BigDecimal(albumInfoIndex.getCommentStatNum()))
                            )
                            .add(
                                    new BigDecimal("0.1")
                                            .multiply(new BigDecimal(albumInfoIndex.getPlayStatNum()))
                            );
            albumInfoIndex.setHotScore(hotScore.doubleValue());
            return albumListVo;
        }, ThreadPoolConfig.getExecutor());

        //2、查询其它数据   需要1结束后执行
        CompletableFuture<Void> c2 = c1.thenAcceptAsync(albumListVo -> {
            Long userId = albumListVo.getUserId();
            //使用用户id查询 主播昵称
            Result<UserInfoVo> userInfoResult = userInfoFeignClient.getUserInfo(userId);
            UserInfoVo userInfoVo = userInfoResult.getData();
            if (userInfoResult == null
                    || !userInfoResult.getCode().equals(ResultCodeEnum.SUCCESS.getCode())
                    || userInfoVo == null) {
                log.error("上架专辑导入数据，获取主播信息失败,主播id:{}", userId);
                throw new GuiguException(ResultCodeEnum.DATA_ERROR);
            }
            albumInfoIndex.setAnnouncerName(userInfoVo.getNickname());
        }, ThreadPoolConfig.getExecutor());


        // 3、专辑属性   直接可以查询
        CompletableFuture<Void> c3 = CompletableFuture.runAsync(() -> {
            //使用专辑id查询专辑属性
            Result<List<AttributeValueIndex>> attributesResult = albumInfoFeignClient
                    .findAttributesByAlbumId(albumId);
            List<AttributeValueIndex> attributeValueIndexList = attributesResult.getData();
            if (attributesResult == null
                    || CollectionUtils.isEmpty(attributeValueIndexList)
                    || !attributesResult.getCode().equals(ResultCodeEnum.SUCCESS.getCode())
            ) {
                log.error("上架专辑导入数据，获取专辑属性失败,专辑id:{}", albumId);
                throw new GuiguException(ResultCodeEnum.DATA_ERROR);
            }

            albumInfoIndex.setAttributeValueIndexList(attributeValueIndexList);
        }, ThreadPoolConfig.getExecutor());


        //5、分类id： 需要查询专辑的二级和1级分类id  1结束后执行
        CompletableFuture<Void> c4 = c1.thenAcceptAsync(albumListVo -> {
            //远程调用： 根据三级分类id查询base_category_view数据
            Result<BaseCategoryView> attributeByC3IdResult = categoryFeignClient
                    .findAttributeByC3Id(albumListVo.getCategory3Id());
            BaseCategoryView baseCategoryView = attributeByC3IdResult.getData();
            if (attributeByC3IdResult == null
                    || baseCategoryView == null
                    || !attributeByC3IdResult.getCode().equals(ResultCodeEnum.SUCCESS.getCode())) {
                log.error("上架专辑导入数据，获取专辑分类失败,三级分类id:{}", albumListVo.getCategory3Id());
                throw new GuiguException(ResultCodeEnum.DATA_ERROR);

            }
            albumInfoIndex.setCategory1Id(baseCategoryView.getCategory1Id());
            albumInfoIndex.setCategory2Id(baseCategoryView.getCategory2Id());
        }, ThreadPoolConfig.getExecutor());


        //阻塞等待异步编排任务结束
        CompletableFuture.allOf(c1, c2, c3, c4).join();

        //4、保存到es中
        elasticsearchTemplate.save(albumInfoIndex);
//        elasticsearchTemplate.update()

        //将专辑需要搜索匹配的 字段值 存到搜索提示索引库中
        saveSuggestIndex(albumInfoIndex.getAlbumTitle());
        saveSuggestIndex(albumInfoIndex.getAlbumIntro());
        saveSuggestIndex(albumInfoIndex.getAnnouncerName());

        //同步新上架的专辑到布隆过滤器
        RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConstant.ALBUM_BLOOM_FILTER);
        bloomFilter.add(albumId);

    }

    private void saveSuggestIndex(String value) {
        SuggestIndex suggestIndex = new SuggestIndex();
        suggestIndex.setId(IdUtil.getSnowflakeNextIdStr());
        suggestIndex.setTitle(value);
        suggestIndex.setKeyword(new Completion(new String[]{value}));
        suggestIndex.setKeywordPinyin(new Completion(
                    new String[]{PinYinUtils.toHanyuPinyin(value)}
                )
        );
        suggestIndex.setKeywordSequence(new Completion(
                new String[]{PinYinUtils.getFirstLetter(value)}
        ));
        elasticsearchTemplate.save(suggestIndex);
    }

    @Override
    public void deleteAlbumInfoIndex(Long albumId) {
        if (albumId == null) {
            throw new GuiguException(ResultCodeEnum.DATA_ERROR);
        }
        elasticsearchTemplate.delete(albumId.toString(), AlbumInfoIndex.class);
    }

    @Override
    @SneakyThrows
    public AlbumSearchResponseVo searchAlbums(AlbumIndexQuery query) {
//        NativeQueryBuilder builder = NativeQuery.builder();
        SearchRequest.Builder builder = new SearchRequest.Builder();
        //1、获取参数：组装布尔组合查询的条件
        BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
        String keyword = query.getKeyword();
        if (!StringUtils.isEmpty(keyword)) {
            boolQueryBuilder.must(bqm ->
                    bqm.bool(
                            bqmb -> bqmb.should(
                                    bq1 -> bq1.match(bq1term -> bq1term.field("albumTitle").query(keyword))
                            ).should(
                                    bq2 -> bq2.match(bq2term -> bq2term.field("albumIntro").query(keyword))
                            ).should(
                                    bq3 -> bq3.term(bq3term -> bq3term.field("announcerName").value(keyword)
                                    )
                            )
                    )
            );
// .fuzziness("1")
        }
        if (query.getCategory1Id() != null) {
            boolQueryBuilder.filter(
                    f -> f.term(ft -> ft.field("category1Id").value(query.getCategory1Id()))
            );
        }
        if (query.getCategory2Id() != null) {
            boolQueryBuilder.filter(
                    f -> f.term(ft -> ft.field("category2Id").value(query.getCategory2Id()))
            );
        }
        if (query.getCategory3Id() != null) {
            boolQueryBuilder.filter(
                    f -> f.term(ft -> ft.field("category3Id").value(query.getCategory3Id()))
            );
        }
        List<String> attributeList = query.getAttributeList();
        if (!CollectionUtils.isEmpty(attributeList)) {
            attributeList.forEach(attribute -> {
                //"10:21"
                String[] split = attribute.split(":");
                if (split != null && split.length == 2) {
                    String attrId = split[0];
                    String valueId = split[1];
                    boolQueryBuilder.filter(
                            f -> f.nested(fn -> fn.path("attributeValueIndexList")
                                    .query(q -> q.bool(
                                            qb -> qb.must(
                                                    m1 -> m1.term(m1t ->
                                                            m1t.field("attributeValueIndexList.attributeId").value(attrId))
                                            ).must(
                                                    m2 -> m2.term(m2t -> m2t.field("attributeValueIndexList.valueId").value(valueId))
                                            )
                                    )))
                    );
                }
            });
        }


        //  布尔组合： 匹配查询
        //  布尔组合： 过滤查询：  三级分类id、属性和值id的嵌套查询
        builder.query(q -> q.bool(boolQueryBuilder.build()));// 将bool组合查询 组装的DSL语句构建设置给builder
        //2、排序  2:desc
        String order = query.getOrder();
        if(!StringUtils.isEmpty(order)){
            String[] split = order.split(":");
            if(split!=null && split.length==2){
                switch (split[0]){
                    case "2":
                        builder.sort(s->s.field(fs->fs.field("playStatNum")
                                .order(split[1].equalsIgnoreCase("desc")? SortOrder.Desc:SortOrder.Asc)));
                        break;
                    case "3":
                        builder.sort(s->s.field(fs->fs.field("createTime")
                                .order(split[1].equalsIgnoreCase("desc")? SortOrder.Desc:SortOrder.Asc)));
                        break;
                }
            }
            //如果没有设置排序字段，默认按照匹配分数降序排列
        }

        //3、分页
        //查询的起始索引
        int index = (query.getPageNo()-1)* query.getPageSize();
        builder.from(index);
        builder.size(query.getPageSize());
        //高亮
        builder.highlight(Highlight.of(h->
                h.fields("albumTitle" ,
                    f->f.preTags("<font style='color:red;'>").postTags("</font>") )
                .fields("albumIntro" ,
                    f->f.preTags("<font style='color:red;'>").postTags("</font>") )
                .fields("announcerName" ,
                    f->f.preTags("<font style='color:red;'>").postTags("</font>") )
        ));

        //4、解析查询的结果集
        //elasticsearchTemplate:高度封装，使用方便，但是可操作性低
//        elasticsearchTemplate.search(builder.build()  ,AlbumInfoIndex.class);

//        System.out.println(builder.build());
        SearchRequest build = builder.build();
        //打印构建的DSL语句
        System.out.println(build);
        SearchResponse<AlbumInfoIndex> response = elasticsearchClient.search(build, AlbumInfoIndex.class);

        AlbumSearchResponseVo vo = getAlbumSearchResponseVo(query, response);
        return vo;
    }


    private static AlbumSearchResponseVo getAlbumSearchResponseVo(AlbumIndexQuery query, SearchResponse<AlbumInfoIndex> response) {
        //解析结果：
        // 前端如何使用结果(后端按照前端要求组装数据)
        AlbumSearchResponseVo vo = new AlbumSearchResponseVo();
        vo.setPageNo(query.getPageNo());
        vo.setPageSize(query.getPageSize());
        //获取总记录条数
        long total = response.hits().total().value();

        vo.setTotal(total);
        //计算总页码
        long totalPage = total%vo.getPageSize()==0?total/vo.getPageSize() :
                (total/vo.getPageSize()+1);
        vo.setTotalPages(totalPage);
        List<AlbumInfoIndexVo> albumInfoIndexVos = new ArrayList<>();
        response.hits().hits().forEach(h -> {
//            System.out.println(h);
            AlbumInfoIndex infoIndex = h.source();
            AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
            BeanUtils.copyProperties(infoIndex , albumInfoIndexVo);

            //获取查询到的高亮字段
            h.highlight().forEach((k,v)->{
                if("albumTitle".equals(k)){
                    albumInfoIndexVo.setAlbumTitle(v.get(0));
                }else if("albumIntro".equals(k)){
                    albumInfoIndexVo.setAlbumIntro(v.get(0));
                }else if("announcerName".equals(k)){
                    albumInfoIndexVo.setAnnouncerName(v.get(0));
                }
            });

            albumInfoIndexVos.add(albumInfoIndexVo);
        });
        //设置 查询到的专辑列表
        vo.setList(albumInfoIndexVos);
        return vo;
    }
}
